package com.barney.support.http;

import java.util.List;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.lang.ArrayUtils;
import org.springframework.util.Assert;

/**
 * 멀티쓰레드 컨넥션 관리자.
 * HTTP 요청을 처리하고 응답을 돌려준다.
 * 요청에 대한 응답은 {@link HttpInvocation} 객체가 처리한다.
 * 사용 예:
 * <p>
 * &lt;bean id="httpClientManager" class="com.barney.support.http.HttpClientManager" destroy-method="destroy"&gt;<br/>
 * &lt;property name="maxTotalConnections" value="20"/&gt;<br/>
 * &lt;property name="timeout" value="3000"/&gt;<br/>
 * &lt;/bean&gt;<br/>
 * </p>
 *
 * @see <a href="http://hc.apache.org/httpclient-legacy/threading.html">Theading</a>
 */
public class HttpClientManager {

	private final MultiThreadedHttpConnectionManager connectionManager;

	private boolean staleCheckingEnabled = true;

	private int maxConnectionsPerHost = 20;

	private int maxTotalConnections = 20;

	private int timeout;

	/**
	 * 멀티쓰레드 컨넥션 관리자를 생성한다.
	 */
	public HttpClientManager() {
		connectionManager = new MultiThreadedHttpConnectionManager();
		final HttpConnectionManagerParams cmp = connectionManager.getParams();
		cmp.setStaleCheckingEnabled(staleCheckingEnabled);
		cmp.setDefaultMaxConnectionsPerHost(maxConnectionsPerHost);
		cmp.setMaxTotalConnections(maxTotalConnections);
	}

	/**
	 * 멀티쓰레드 컨넥션 관리자를 종료한다.
	 */
	public void destroy() {
		connectionManager.shutdown();
	}

	public boolean isStaleCheckingEnabled() {
		return staleCheckingEnabled;
	}

	public void setStaleCheckingEnabled(boolean staleCheckingEnabled) {
		this.staleCheckingEnabled = staleCheckingEnabled;
	}

	public int getMaxConnectionsPerHost() {
		return maxConnectionsPerHost;
	}

	public void setMaxConnectionsPerHost(int maxConnectionsPerHost) {
		this.maxConnectionsPerHost = maxConnectionsPerHost;
	}

	public int getMaxTotalConnections() {
		return maxTotalConnections;
	}

	public void setMaxTotalConnections(int maxTotalConnections) {
		this.maxTotalConnections = maxTotalConnections;
	}

	public int getTimeout() {
		return timeout;
	}

	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	/**
	 * HTTP 클라이언트를 반환한다.
	 */
	public HttpClient getHttpClient() {
		HttpClient httpClient = new HttpClient(connectionManager);
		if (timeout > 0) {
			httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(timeout);
			httpClient.getHttpConnectionManager().getParams().setSoTimeout(timeout);
		}
		return httpClient;
	}

	/**
	 * GET 요청을 처리한다.
	 *
	 * @param baseUrl 기본 URL
	 * @param httpInvocation
	 */
	public <T> T get(String baseUrl, HttpInvocation<T> httpInvocation) {
		Assert.notNull(baseUrl, "baseUrl은 null일 수 없다.");
		Assert.notNull(httpInvocation, "httpInvocation 값은 null일 수 없다.");

		GetMethod getMethod = new GetMethod(baseUrl + httpInvocation.getUri());
		if (ArrayUtils.isNotEmpty(httpInvocation.getParameters())) {
			getMethod.setQueryString(httpInvocation.getParameters());
		}
		return invoke(getMethod, httpInvocation);
	}

	/**
	 * POST 요청을 처리한다.
	 *
	 * @param baseUrl
	 * @param httpInvocation
	 */
	public <T> T post(String baseUrl, HttpInvocation<T> httpInvocation) {
		Assert.notNull(baseUrl, "baseUrl은 null일 수 없다.");
		Assert.notNull(httpInvocation, "httpInvocation 값은 null일 수 없다.");

		PostMethod postMethod = new PostMethod(baseUrl + httpInvocation.getUri());

		String parameterEncoding = httpInvocation.getParameterEncoding();
		if (parameterEncoding != null && !parameterEncoding.isEmpty()) {
			postMethod.getParams().setContentCharset(parameterEncoding);
		}

		postMethod.addParameters(httpInvocation.getParameters());

		return invoke(postMethod, httpInvocation);
	}

	/**
	 * HTTP 요청 결과를 HttpInvocation 객체에 위임하여 결과를 가공해 리턴한다.
	 *
	 * @param httpMethod
	 * @param httpInvocation
	 */
	public <T> T invoke(HttpMethod httpMethod, HttpInvocation<T> httpInvocation) {
		populateCookiePolicy(httpMethod);
		populateRequestHeaders(httpMethod, httpInvocation);

		final HttpClient httpClient = getHttpClient();
		if (httpInvocation.getUserAgent() != null) {
			httpClient.getParams().setParameter(HttpMethodParams.USER_AGENT,
					httpInvocation.getUserAgent());
		}
		try {
			httpClient.executeMethod(httpMethod);

			processOnResponses(httpMethod, httpInvocation);
		} catch (Exception ex) {
			httpInvocation.onException(ex);
		} finally {
			httpMethod.releaseConnection();
		}

		return httpInvocation.getReturnValue();
	}

	protected <T> void populateRequestHeaders(HttpMethod httpMethod,
			HttpInvocation<T> httpInvocation) {
		List<Header> requestHeaders = httpInvocation.getRequestHeaders();
		if (CollectionUtils.isNotEmpty(requestHeaders)) {
			for (Header header : requestHeaders) {
				httpMethod.addRequestHeader(header);
			}
		}
	}

	protected void populateCookiePolicy(HttpMethod httpMethod) {
		httpMethod.getParams().setCookiePolicy(CookiePolicy.RFC_2109);
	}

	protected <T> void processOnResponses(HttpMethod httpMethod, HttpInvocation<T> httpInvocation)
			throws Exception {
		int statusCode = httpMethod.getStatusCode();
		httpInvocation.onResponseStatus(statusCode);

		Header[] responseHeaders = httpMethod.getResponseHeaders();
		if (ArrayUtils.isNotEmpty(responseHeaders)) {
			for (Header header : responseHeaders) {
				httpInvocation.onResponseHeader(header.getName(), header.getValue());
			}
		}

		httpInvocation.onResponseBody(httpMethod.getResponseBodyAsStream());
	}
}
